核心文件:
src/utils/hooks/(16 个文件,3721 行)
入口:src/utils/hooks.ts(主调度器)
一、系统定位
Hooks 是 Claude Code 可扩展性的核心机制:用户(和企业管理员)可以在 Agent 生命周期的任意关键节点注入自定义逻辑,而不需要修改 CC 本身。
三类用途:
- 观察:记录工具调用日志、发送通知
- 拦截:阻止危险操作(exit 2 blocking)
- 增强:向 Claude 注入额外上下文(exit 0 stdout)
二、27 个钩子事件(完整表)
来源:src/utils/hooks/hookEvents.ts(getHookEventMetadata 函数)
工具生命周期(4 个)
| 事件 | 触发时机 | 支持 matcher | 阻断能力 |
|---|---|---|---|
PreToolUse |
工具调用前 | tool_name |
✅ exit 2 阻止工具执行 |
PostToolUse |
工具调用后(成功) | tool_name |
⚠️ exit 2 向 model 发 stderr |
PostToolUseFailure |
工具调用失败后 | tool_name |
⚠️ exit 2 向 model 发 stderr |
PermissionDenied |
自动模式分类器拒绝工具 | tool_name |
可输出 retry:true 让 model 重试 |
PreToolUse exit 协议:
- exit 0:静默通过,stdout/stderr 不显示
- exit 2:向 model 发 stderr 并阻止 工具执行
- 其他:向用户显示 stderr,继续执行工具
PostToolUse exit 协议:
- exit 0:stdout 显示在 transcript 模式(Ctrl+O)
- exit 2:立即向 model 发 stderr(可影响下一轮)
- 其他:向用户显示 stderr
会话生命周期(4 个)
| 事件 | 触发时机 | matcher | 特殊行为 |
|---|---|---|---|
SessionStart |
新会话启动 | source(startup/resume/clear/compact) |
exit 0 stdout 注入 Claude;始终广播(SDK always-emit) |
SessionEnd |
会话结束前 | reason(clear/logout/prompt_input_exit/other) |
无阻断 |
Stop |
Claude 准备结束回复前 | 无 | exit 2 继续对话(让 Claude 再想想) |
StopFailure |
API 错误结束 turn | error(rate_limit/auth 等) |
Fire-and-forget,输出全忽略 |
子 Agent 生命周期(2 个)
| 事件 | 触发时机 | matcher | exit 0 行为 |
|---|---|---|---|
SubagentStart |
Agent 工具调用启动 | agent_type |
stdout 注入子 Agent |
SubagentStop |
子 Agent 结束前 | agent_type |
exit 2 让子 Agent 继续运行 |
压缩生命周期(2 个)
| 事件 | 触发 | matcher | 阻断 |
|---|---|---|---|
PreCompact |
上下文压缩前 | trigger(manual/auto) |
exit 0 stdout 追加自定义压缩指令;exit 2 阻止压缩 |
PostCompact |
压缩后 | trigger |
无阻断,stdout 显示给用户 |
用户交互(3 个)
| 事件 | 触发 | 阻断 |
|---|---|---|
UserPromptSubmit |
用户提交 prompt | exit 2 阻止处理并清除原始 prompt;exit 0 stdout 注入 Claude |
Notification |
发送通知时 | notification_type(permission_prompt/idle_prompt 等) |
PermissionRequest |
权限对话框弹出时 | 输出 JSON hookSpecificOutput.decision 可程序化允许/拒绝 |
MCP Elicitation(2 个)
| 事件 | 触发 | 特殊输出 |
|---|---|---|
Elicitation |
MCP 服务端请求用户输入 | 输出 hookSpecificOutput.action(accept/decline/cancel)可自动响应 |
ElicitationResult |
用户响应 MCP elicitation 后 | exit 2 阻止响应(变为 decline) |
文件与目录(3 个)
| 事件 | 触发 | 特殊机制 |
|---|---|---|
CwdChanged |
工作目录变更 | CLAUDE_ENV_FILE 注入环境变量;可返回 watchPaths 注册文件监控 |
FileChanged |
被监控文件变化 | matcher 是文件名模式(如 .envrc|.env);同样支持 CLAUDE_ENV_FILE |
WorktreeCreate / WorktreeRemove |
Worktree 创建/删除 | stdout 必须是绝对路径(Create);无阻断(Remove) |
多 Agent 任务(3 个)
| 事件 | 触发 | 阻断 |
|---|---|---|
TeammateIdle |
Teammate 即将空闲 | exit 2 向 teammate 发 stderr 并阻止空闲 |
TaskCreated |
创建任务时 | exit 2 阻止任务创建 |
TaskCompleted |
标记任务完成时 | exit 2 阻止任务完成 |
配置与指令(2 个)
| 事件 | 触发 | 阻断 |
|---|---|---|
ConfigChange |
会话期间配置文件变更 | exit 2 阻止变更应用 |
InstructionsLoaded |
CLAUDE.md 或规则文件加载 | 仅可观察,不支持阻断 |
仓库维护(1 个)
| 事件 | 触发 | matcher |
|---|---|---|
Setup |
init 或 maintenance 触发 | trigger(init/maintenance);始终广播 |
三、5 种钩子类型
来源:src/utils/settings/types.ts(HookCommand)、src/utils/hooks/hooksSettings.ts(isHookEqual)
1. command(最常用)
{ |
- shell:bash(默认)、powershell、powershell_core、sh、zsh、fish
- if:条件过滤,格式
ToolName(pattern),仅在模式匹配时执行 - timeout:秒(默认 10 分钟,与工具钩子超时一致)
- 输入通过 stdin 接收 JSON
执行链路:hooks.ts:runHook() → spawn() → 读 stdout/stderr → 解析 exit code
2. prompt(LLM 单轮)
{ |
$ARGUMENTS占位符在执行时替换为钩子 JSON 输入(addArgumentsToPrompt)- 默认使用
getSmallFastModel()(Haiku) - 单次
queryModelWithoutStreaming()调用,默认 30s 超时 - 响应必须是 JSON:
{"ok": true}或{"ok": false, "reason": "..."} - ok=false 时等价于 exit 2(blocking)
实现:src/utils/hooks/execPromptHook.ts
3. agent(LLM 多轮)
{ |
- 使用完整
query()引擎,最多 50 轮对话 - 可访问所有工具(除
ALL_AGENT_DISALLOWED_TOOLS:不能启动子 Agent 或进入 PlanMode) - 通过
SyntheticOutputTool返回结构化输出{ok, reason?} - 自动注入 transcript 路径权限,Agent 可读取对话历史
- 未在 50 轮内返回结果 → outcome: ‘cancelled’(不报错)
- 分析事件:
tengu_agent_stop_hook_*
实现:src/utils/hooks/execAgentHook.ts
4. http(HTTP POST)
{ |
- POST JSON 到指定 URL,body = 钩子输入 JSON
- header 环境变量插值:
$VAR/${VAR}→ 仅allowedEnvVars中的变量被展开,防止 secrets 泄露 - SSRF 防护(见下节)
- 企业策略:
allowedHttpHookUrls(URL 白名单)+httpHookAllowedEnvVars(全局 env 变量白名单) - 禁用 axios 自动重定向(maxRedirects: 0)
实现:src/utils/hooks/execHttpHook.ts
5. function(TypeScript 回调,仅会话作用域)
addFunctionHook(setAppState, sessionId, 'PreToolUse', 'Bash', |
- 不可持久化:仅存 AppState 内存,会话结束即清除
- 有返回 ID,可通过
removeFunctionHook(id)移除 - 超时默认 5s(比 command 短得多)
- 无法通过 settings.json 配置,只能 SDK 编程注入
四、配置源与优先级
来源:src/utils/hooks/hooksSettings.ts:getAllHooks()
优先级(高 → 低): |
去重机制:多个源指向同一个 settings.json 文件时(如在 home 目录运行,userSettings = projectSettings),通过 seenFiles: Set<string> 跳过重复文件。
企业锁定:policySettings.allowManagedHooksOnly = true 时,userSettings/projectSettings/localSettings 的钩子全部忽略,只有管理员定义的 hooks 生效。
五、SessionHooks:会话级动态钩子
来源:src/utils/hooks/sessionHooks.ts
存储结构
type SessionHooksState = Map<string, SessionStore> |
为什么用 Map 而不是 Record?
注释原话:高并发场景下,parallel() 启动 N 个 schema-mode agents 时,N 个 addFunctionHook 在同一个同步 tick 触发。Record + spread 是 O(N²)(每次 spread 复制增长的 Map)且触发所有 ~30 个 store listeners;Map.set() 是 O(1),返回 prev 不触发 listener。
操作 API
// 添加命令钩子(临时) |
Function 钩子 vs Command 钩子
| 维度 | function | command/prompt/agent/http |
|---|---|---|
| 执行位置 | 进程内(TypeScript 回调) | 子进程或外部 HTTP |
| 持久化 | ❌ 仅内存 | ✅ settings.json |
| 超时 | 5s 默认 | 30s-60s 默认 |
| 并发安全 | O(1) Map.set | O(N) spawn |
| 适用场景 | SDK 编程式注入 | 用户配置 |
六、AsyncHookRegistry:异步钩子注册表
来源:src/utils/hooks/AsyncHookRegistry.ts
异步协议
钩子可以在 stdout 输出 {"async": true, "asyncTimeout": 15000} 后继续后台运行(不阻塞主流程)。
钩子进程 stdout: {"async": true, "asyncTimeout": 15000} |
核心数据结构
type PendingAsyncHook = { |
轮询与清理
// 在 query loop 每轮调用 |
SessionStart 特殊处理:SessionStart 的异步钩子完成后,自动调用 invalidateSessionEnvCache() 使环境变量缓存失效,确保下一次工具调用使用最新环境。
进度事件:每 1000ms 轮询 shellCommand.taskOutput,有新 stdout 时通过 emitHookProgress 广播。
七、SSRF 防护(HTTP 钩子专用)
来源:src/utils/hooks/ssrfGuard.ts
阻断的 IP 范围
IPv4 阻断: |
防 DNS 重绑定
ssrfGuardedLookup 作为 axios lookup 回调,在 DNS 解析完成后、socket 连接前验证 IP,消除 Time-of-Check-to-Time-of-Use 窗口。
代理绕过:当沙箱代理或环境变量代理生效时,跳过 SSRF guard(代理本身执行 DNS,guard 会错误地检查代理的 IP)。
IPv4-mapped IPv6 防绕过
::ffff:169.254.169.254 → 提取 169.254.169.254 → 阻断 |
expandIPv6Groups() 先展开 :: 压缩形式,再提取嵌入的 IPv4 地址,避免 hex 形式绕过。
八、Hook 事件广播系统
来源:src/utils/hooks/hookEvents.ts
事件类型
type HookStartedEvent = { type: 'started'; hookId; hookName; hookEvent } |
广播策略
| 模式 | 总是广播 | 可选广播(需开启) |
|---|---|---|
| 默认 | SessionStart、Setup | 其余 25 个事件 |
SDK includeHookEvents: true 或 CLAUDE_CODE_REMOTE |
全部 27 个 | — |
setAllHookEventsEnabled(true) 开启全量广播。
Pending Buffer
handler 注册前积压的事件存入 pendingEvents(上限 100 条,超出丢弃最旧)。handler 注册时立即 replay 所有积压事件。
九、Hooks 配置快照机制
来源:src/utils/hooks/hooksConfigSnapshot.ts
Hooks 配置在 session 初始化时快照一次(避免运行中配置热更新带来不确定性)。关键函数:
getHooksConfigFromSnapshot() // 使用快照中的 hooks |
ConfigChange 事件本身由文件监控触发,hook 可 exit 2 阻止新配置应用到当前会话。
十、面试必考点
Q1:Claude Code 的钩子系统如何工作?能实现哪些能力?
答:Hooks 是用户/企业在 Agent 生命周期关键节点注入自定义逻辑的机制。27 个事件覆盖工具调用前后、会话开始/结束、用户输入提交、压缩、权限请求等全链路。
核心能力三类:
- 观察:记录日志(exit 0,stdout 不打扰 model)
- 阻断:exit 2 阻止危险操作(如 PreToolUse 阻止写生产数据库)
- 增强:exit 0 stdout 向 Claude 注入上下文(如 SessionStart 注入团队规范)
Q2:exit code 协议是什么?
答:不同事件的 exit code 语义不同,但通用模式:
- exit 0:成功,stdout 可能显示给 Claude 或用户(事件决定)
- exit 2:阻断,stderr 发给 model 或阻止动作(事件决定)
- 其他非零:向用户显示 stderr,主流程继续
PreToolUse exit 2 = 阻止工具执行 + 向 model 发错误;Stop exit 2 = 继续对话(Claude 再想想)。
Q3:5 种钩子类型各有什么适用场景?
| 类型 | 适用场景 | 性能开销 |
|---|---|---|
| command | 日志、通知、调用现有脚本 | 低(子进程) |
| prompt | 简单 LLM 验证(30s 超时) | 中(单次 API 调用) |
| agent | 复杂验证,需要读文件/工具(50 轮) | 高(多轮 query) |
| http | 企业策略服务、外部审批 | 低(HTTP POST) |
| function | SDK 编程式注入(不持久化) | 极低(进程内) |
Q4:SessionHooks 为什么用 Map 而不是 Record?
答:高并发场景(parallel() 启动 N 个 agent 在同一 tick 调用 addFunctionHook):
- Record + spread:O(N²) 复制成本 + 触发 ~30 个 store listeners
- Map:O(1) set +
return prev(引用不变)→ Object.is 检查短路,零 listener 触发
Q5:HTTP 钩子的安全机制是什么?
答:三层防护:
- URL 白名单:
allowedHttpHookUrls(企业管理员设置,*通配符) - SSRF 防护:
ssrfGuardedLookup在 DNS 解析完成后检查 IP,阻断私有/链路本地地址;特别处理 IPv4-mapped IPv6 防绕过 - env 变量白名单:header 中的
$VAR插值只展开allowedEnvVars中的变量,防止 secrets 泄露;结果 sanitize 去除\r\n\x00防 CRLF 注入
Q6:异步钩子如何不阻塞主流程?
答:钩子进程在 stdout 输出 {"async": true} 后即返回,主流程继续。AsyncHookRegistry 保存对进程的引用,query loop 在每轮 checkForAsyncHookResponses() 轮询是否完成,完成后注入响应。SessionStart 的异步钩子完成后额外触发 invalidateSessionEnvCache()。
Q7:企业如何锁定钩子?
答:allowManagedHooksOnly = true → userSettings/projectSettings/localSettings 的钩子全部忽略,只有管理员在 policySettings 中定义的 hooks 生效。shouldDisableAllHooksIncludingManaged = true → 完全禁用钩子,包括 managed hooks。


